/*
 * This file is part of the <a href="http://code.google.com/p/pux">PUX Framework</a>
 *
 * Copyright 2008 by <a href="mailto:jfendler@polytechnic.edu.na">Jens Fendler</a>
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *
 */

package de.teamskill.pux.engine;

import java.util.HashMap;
import java.util.Map;

/**
 * The Network class represents a complete network consisting of nodes and
 * connection between their ports. Each Network instance maintains its own
 * thread pool, allowing to keep track of the (parallel) execution of all nodes
 * within one network.
 * 
 * @see AbstractNode
 * @see Port
 * @see ThreadGroup
 * 
 * @author <a href="mailto:jfendler@polytechnic.edu.na">Jens Fendler</a>
 * 
 */
public class Network implements Logging {

	/**
	 * Map of all node names and nodes within the network
	 */
	private Map<String, AbstractNode> nodes = new HashMap<String, AbstractNode>();

	/**
	 * The thread group used for this network (instantiated within constructor)
	 * 
	 * @see ThreadGroup
	 */
	private ThreadGroup threadGroup;

	/**
	 * If true, the network waits for the termination of the last running thread
	 * and prints out the total run time of this network
	 */
	private boolean printTiming = false;

	/**
	 * Internal counter used for automatic naming of thread groups
	 */
	private static int nr = 1;

	/**
	 * Network Constructor
	 */
	public Network() {
		super();
		threadGroup = new ThreadGroup(getClass().getName() + nr++);
	}

	/**
	 * Add a new node into the network
	 * 
	 * @param n
	 *            the node to add to the network
	 */
	public void addNode(AbstractNode n) {
		n.setNetwork(this);
		n.createPorts();
		nodes.put(n.getNodeName(), n);
	}

	/**
	 * connect
	 * 
	 * @param sourceNode
	 * @param sourcePort
	 * @param targetNode
	 * @param targetPort
	 */
	public void connect(AbstractNode sourceNode, String sourcePort, AbstractNode targetNode, String targetPort) {
		connect(sourceNode.getOutputPort(sourcePort), targetNode.getInputPort(targetPort));
	}

	/**
	 * Connect an output port with an input port
	 * 
	 * @param op
	 * @param ip
	 */
	public void connect(OutputPort op, InputPort ip) {
		if (!nodes.containsValue(op.getNode())) {
			throw new IllegalArgumentException("AbstractNode " + op.getNode().getNodeName()
					+ " of output port [" + op.getName()
					+ "] is not (yet) part of the network. Not connecting.");
		}
		if (!nodes.containsValue(ip.getNode())) {
			throw new IllegalArgumentException("AbstractNode " + ip.getNode().getNodeName()
					+ " of input port [" + ip.getName()
					+ "] is not (yet) part of the network. Not connecting.");
		}
		op.setRemotePort(ip);
		ip.setRemotePort(op);
	}

	/**
	 * connect - convenience method to connect a source node with only one
	 * output port with a target node with only one input port
	 * 
	 * @param sourceNode
	 * @param targetNode
	 */
	public void connect(AbstractNode sourceNode, AbstractNode targetNode) {
		connect(sourceNode.getOutputPort(), targetNode.getInputPort());
	}

	/**
	 * Disconnect a node's port, i.e. set its remote port attribute to null
	 * 
	 * @param port
	 *            the port to disconnect
	 */
	public void disconnect(Port port) {
		port.getRemotePort().setRemotePort(null);
		port.setRemotePort(null);
	}

	/**
	 * Start the network execution by calling the process() methods of all nodes
	 * which have their startNode property set to true.
	 */
	public void start() {
		LOG_NET.fine("Starting Network execution...");
		long t0 = System.currentTimeMillis();
		for (AbstractNode n : nodes.values()) {
			if (n.isStartupNode() && !n.isRunning()) {
				n.startNode();
			}
		}

		// TODO following code block causes potential problems. use wait/notify!

		// if timing should be printed, wait for all threads to exit
		if (printTiming) {
			while (threadGroup.activeCount() > 0) {
				try {
					Thread.sleep(10);
				} catch (InterruptedException e) {
				}
			}
			long total = System.currentTimeMillis() - t0;
			LOG_NET.info("Network finished: " + total + "ms.");
		}
	}

	/**
	 * @return the threadGroup
	 */
	public ThreadGroup getThreadGroup() {
		return threadGroup;
	}

	/**
	 * @param threadGroup
	 *            the threadGroup to set
	 */
	public void setThreadGroup(ThreadGroup threadGroup) {
		this.threadGroup = threadGroup;
	}

	/**
	 * @return the printTiming
	 */
	public boolean isPrintTiming() {
		return printTiming;
	}

	/**
	 * @param printTiming
	 *            the printTiming to set
	 */
	public void setPrintTiming(boolean printTiming) {
		this.printTiming = printTiming;
	}

	/**
	 * @return the nodes
	 */
	public Map<String, AbstractNode> getNodes() {
		return nodes;
	}

	/**
	 * @param nodes
	 *            the nodes to set
	 */
	public void setNodes(Map<String, AbstractNode> nodes) {
		this.nodes = nodes;
	}

}
